package org.epidroid.epidroid.androsoul.engine;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StreamCorruptedException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;

import org.epidroid.epidroid.androsoul.engine.communication.Connection;
import org.epidroid.epidroid.androsoul.engine.communication.Connection.State;
import org.epidroid.epidroid.androsoul.engine.communication.Query;
import org.epidroid.epidroid.androsoul.engine.communication.QueryQueue;
import org.epidroid.epidroid.androsoul.engine.contacts.ContactDictionary;
import org.epidroid.epidroid.androsoul.engine.contacts.ContactGroup;
import org.epidroid.epidroid.androsoul.engine.contacts.Contacts;
import org.epidroid.epidroid.androsoul.engine.contacts.ContactsLoader;
import org.epidroid.epidroid.androsoul.engine.contacts.ContactsSaver;
import org.epidroid.epidroid.androsoul.engine.contacts.Location;
import org.epidroid.epidroid.androsoul.engine.contacts.NetsoulContact;
import org.epidroid.epidroid.androsoul.engine.conversation.Conversation;
import org.epidroid.epidroid.androsoul.engine.conversation.MessageItem;
import org.epidroid.epidroid.androsoul.engine.conversation.OpenedConversations;
import org.epidroid.epidroid.androsoul.engine.utils.Printer;
import org.epidroid.epidroid.androsoul.engine.utils.StringUtils;
import org.epidroid.epidroid.androsoul.exceptions.AndroSoulException;

import android.os.Handler;
import android.os.Message;
import android.util.Log;

public class AndroSoul {
	public static final String LOGCAT = "androsoul";
	public static final String PIC_URL = "https://www.epitech.eu/intra/photo.php?login=";

	public static final String DEFAULT_GROUP_NAME = "My contacts";

	// Messages code
	public static final int EXCEPTION = -3;
	public static final int DECONNECTED = -2;
	public static final int NONE = -1;
	public static final int SALUT = 0;
	public static final int AUTH_AG = 1;
	public static final int EXT_USER_LOG = 2;
	public static final int CONTACT_WHO_RESULT = 3;
	public static final int RECEIVE_MESSAGE = 4;
	public static final int LIST_USERS = 5;
	public static final int USER_CMD = 6;
	public static final int CONTACT_LOGOUT = 7;
	public static final int LOGIN = 8;
	public static final int CONTACT_STATE_UPDATE = 9;
	public static final int EXIT = 10;
	public static final int PING = 11;
	public static final int WIZZ = 12;
	public static final int USER_TYPING = 13;
	public static final int USER_CANCEL_TYPING = 14;
	// Myself
	public NetsoulContact _myself = null;
	private String _comment = "none";
	private String _location = "none";
	// The connection object, used to handle non authed commands
	private Connection _connection = null;
	// The dictionary gathering all contacts
	private ContactDictionary _dictionary = null;
	// The object containing groups and contacts
	private Contacts _contacts = null;
	// Query queue
	private QueryQueue _queryQueue = null;
	// List of all opened conversations
	private OpenedConversations _openedConversations = null;
	// Handler that enables the listener to send messages
	private Handler _listenerHandler = null;
	// Handler to dispatch actions to users
	private Handler _userHandler = null;
	private String _login = null;
	private String _pass = null;
	private boolean _enableWizzs;

	/**
	 * Create the contact list, send authentication query, start listener
	 * 
	 * @param handler
	 *            Handler for the listener.
	 */
	public AndroSoul(Handler handler) {
		Log.d(AndroSoul.LOGCAT, "Creating AndroSoul instance");
		_userHandler = handler;
		_dictionary = new ContactDictionary(this);
		_contacts = new Contacts(this);
		_openedConversations = new OpenedConversations();

		initHandler();
	}

	/**
	 * Create the connection object Send the connection request, initialize the
	 * query queue
	 * 
	 * @param login
	 *            login used for this connection
	 * @param pass
	 *            password used for this connection
	 * @throws AndroSoulException
	 */
	public Boolean connect(String login, String pass) {

		/*
		 * Try and connect only if we are deconnected (not connected and not
		 * already connecting)
		 */

		if ((_connection != null) && (_connection.getState() != State.OFF)
				&& (_connection.getState() != State.LOGOUT)) {
			Log.e(LOGCAT, "Cannot reconnect, connection state is "
					+ _connection.getState().toString());
			return false;
		}

		_login = login;
		_pass = pass;
		initMyself();
		_connection = new Connection();
		_queryQueue = new QueryQueue(_connection);
		try {
			_connection.connect();
			_connection.listen(_listenerHandler, _queryQueue);
		} catch (AndroSoulException e) {
			Message _m = new Message();
			_m.what = AndroSoul.EXCEPTION;
			_m.obj = e;
			_userHandler.sendMessage(_m);
			Log.v(LOGCAT, (e.getMessage()));
		}
		return true;
	}

	/**
	 * Try and reconnect when connection has been lost. If reconnection fails,
	 * inform listeners and try again later.
	 */
	private void reConnect() {

		// only reconnect if the user did not ask for logout
		if (_connection.getState() == Connection.State.LOGOUT)
			return;

		Message m = new Message();
		m.what = DECONNECTED;
		_userHandler.sendMessage(m);

		_queryQueue.clear();
		_connection.initDefaultData();
		try {
			_connection.connect();
			_connection.listen(_listenerHandler, _queryQueue);
		} catch (AndroSoulException e) {
			Message _m = new Message();
			_m.what = AndroSoul.EXCEPTION;
			_m.obj = e;
			_userHandler.sendMessage(_m);
			// TODO: try again in a few seconds
		}
	}

	/**
	 * Creation of a contact representing the user who is connected
	 */
	public void initMyself() {
		NetsoulContact c = _dictionary.get(_login);
		if (c == null) {
			_myself = _dictionary.createContact(_login);
		} else {
			_myself = c;
		}
	}

	/**
	 * Initialize the main handler used to dispatch information about events
	 */
	private void initHandler() {
		_listenerHandler = new Handler() {
			@Override
			public void handleMessage(Message msg) {
				switch (msg.what) {
				case AndroSoul.DECONNECTED:
					Log.i(LOGCAT, "Disconnected from server");
					reConnect();
					break;
				case AndroSoul.SALUT:
					receiveSalut(msg);
					break;
				case AndroSoul.AUTH_AG:
					receiveAuthAg(msg);
					break;
				case AndroSoul.EXT_USER_LOG:
					receiveExtUserLog(msg);
					break;
				case AndroSoul.USER_CMD:
					receiveUserCmd(msg);
					break;
				case AndroSoul.CONTACT_WHO_RESULT:
					handleContactWhoResult(msg);
					Message m_ = new Message();
					m_.copyFrom(msg);
					_userHandler.sendMessage(m_);
					break;
				case AndroSoul.LIST_USERS:
					// We have finished listing the users
					Message m = new Message();
					m.copyFrom(msg);
					_userHandler.sendMessage(m);
					break;
				default:
					break;
				}
			}
		};
	}

	/**
	 * Called when the user_cmd command has been received. Look for the command
	 * arguments to determine the proper action described by it.
	 * 
	 * @param msg
	 *            Message object containing command arguments
	 */
	private void receiveUserCmd(Message msg) {
		Message m = null;
		if (msg.getData().getString("command").equals("msg"))
			m = handleReceiveMsg(msg);
		else if (msg.getData().getString("command").equals("logout"))
			m = handleContactLogout(msg);
		else if (msg.getData().getString("command").equals("login"))
			m = handleContactLogin(msg);
		else if (msg.getData().getString("command").equals("state"))
			m = handleContactStateUpdate(msg);
		else if (msg.getData().getString("command").equals("wizz")) {
			if (_enableWizzs)
				m = handleWizz(msg);
			else
				return;
		} else if (msg.getData().getString("command")
				.equals("dotnetSoul_UserTyping"))
			m = handleUserTyping(msg);
		else if (msg.getData().getString("command")
				.equals("dotnetSoul_UserCancelledTyping"))
			m = handleUserCancelTyping(msg);

		// Send info to listener
		if (m == null) {
			m = new Message();
			m.copyFrom(msg);
		}
		_userHandler.sendMessage(m);
	}

	/**
	 * Dispatch information when receiving a "user is typing" command
	 * 
	 * @param msg
	 *            Message received containing command arguments
	 * @return New message to be dispatched, containing reference to the contact
	 *         sending the command
	 */
	private Message handleUserTyping(Message msg) {
		Message res = new Message();
		res.what = USER_TYPING;
		NetsoulContact c = _dictionary.get(msg.getData().getString("login"));
		res.obj = c;
		return res;
	}

	/**
	 * Dispatch information when receiving a "user stops typing" command
	 * 
	 * @param msg
	 *            Message received containing command arguments
	 * @return New message to be dispatched, containing reference to the contact
	 *         sending the command
	 */
	private Message handleUserCancelTyping(Message msg) {
		Message res = new Message();
		res.what = USER_CANCEL_TYPING;
		NetsoulContact c = _dictionary.get(msg.getData().getString("login"));
		res.obj = c;
		return res;
	}

	/**
	 * Dispatch information when receiving a wizz command (this command is
	 * specific to AndroSoul and thus may only be received from a contact using
	 * AndroSoul)
	 * 
	 * @param msg
	 *            Message received containing command arguments
	 * @return New message to be dispatched, containing reference to the contact
	 *         sending the command
	 */
	private Message handleWizz(Message msg) {
		Message res = new Message();
		res.what = WIZZ;
		res.obj = msg.getData().getString("login");
		
		return res;
	}

	/**
	 * Called when receiving the "ext user log" command acknowledgment from the
	 * netsoul server. If the response is 002, the authentication is a success.
	 * 
	 * @param msg
	 *            Message received containing command arguments
	 * @return New message to be dispatched, clone of the above
	 */
	private void receiveExtUserLog(Message msg) {
		if (msg.arg1 == 2) {
			_connection.setState(Connection.State.AUTHENTIFIED);
			// Change the state to "actif"
			// TODO: use constants to define default states
			changeUserState("actif");
			Log.v(AndroSoul.LOGCAT, "Authentication success");
		} else {
			// TODO: better handling of errors
			_connection.setState(Connection.State.OFF);
		}

		// Dispatch information
		Message m = new Message();
		m.copyFrom(msg);
		_userHandler.sendMessage(m);
	}

	/**
	 * Called when the netsoul server sends an authentication agreement (after
	 * we sent the "auth_ag" command). Change the connection state to
	 * "CAN_AUTH". Send the authentication command.
	 * 
	 * @param msg
	 *            Message containing command arguments
	 */
	private void receiveAuthAg(Message msg) {
		Log.v(AndroSoul.LOGCAT, "Authorized for authentification");
		_connection.setState(Connection.State.CAN_AUTH);
		sendAuthQuery(_login, _pass);
	}

	/**
	 * Called when the server sends the first message after connection.
	 * 
	 * @param msg
	 *            Message containing the command arguments. These arguments are
	 *            saved and later used to start authentication.
	 */
	private void receiveSalut(Message msg) {
		Log.v(AndroSoul.LOGCAT, "Connected to the server");
		_connection.initData(msg.getData());
		askForAuth();
	}

	/**
	 * Called when received a location / state information for a contact
	 * 
	 * @param msg
	 *            Message containing the command arguments Retrieve the contact,
	 *            add the location
	 */
	private void handleContactWhoResult(Message msg) {
		String author = msg.getData().getString("login");
		NetsoulContact c = _dictionary.get(author);
		if (c != null) {
			c.addLocation(new Location(msg.getData().getString("user_host"),
					msg.getData().getString("location")));
			c.setState(msg.getData().getString("state"));
			c.setComment(msg.getData().getString("comment"));
		}
		// TODO: Handle other information given by the who command's results
		// (state, �)
	}

	/**
	 * Called when a contact logs out.
	 * 
	 * @param msg
	 *            Message containing the arguments
	 * @return New message to be displayed, containing referenced to the contact
	 *         in the dictionary
	 */
	private Message handleContactLogout(Message msg) {
		String author = msg.getData().getString("login");

		NetsoulContact c = _dictionary.get(author);
		if (c != null)
			c.removeLocation(msg.getData().getString("user_host"));
		else
			return null;
		Message res = new Message();
		res.what = AndroSoul.CONTACT_LOGOUT;
		res.obj = c;
		return res;
	}

	/**
	 * Called when a contact logs in.
	 * 
	 * @param msg
	 *            Message containing the arguments
	 * @return New message to be displayed, containing referenced to the contact
	 *         in the dictionary
	 */
	private Message handleContactLogin(Message msg) {
		String author = msg.getData().getString("login");

		NetsoulContact c = _dictionary.get(author);
		if (c != null)
			c.addLocation(new Location(msg.getData().getString("user_host"),
					msg.getData().getString("location")));

		else
			return null;
		Message res = new Message();
		res.what = AndroSoul.LOGIN;
		res.obj = c;
		return res;
	}

	/**
	 * Called when a contact changes its state.
	 * 
	 * @param msg
	 *            Message containing the arguments
	 * @return New message to be displayed, containing referenced to the contact
	 *         in the dictionary
	 */
	private Message handleContactStateUpdate(Message msg) {
		String author = msg.getData().getString("login");
		String state = msg.getData().getString("command_ext");
		state = state.replaceAll("\\s+$", "");
		state = state.split(":")[0];
		// TODO: use timestamp (part after the :)

		NetsoulContact c = _dictionary.get(author);
		if (c != null) {
			c.setState(state);
			Log.e(AndroSoul.LOGCAT, "Received state update for " + c.getName());
		} else {
			Log.e(AndroSoul.LOGCAT,
					"Received state for a contact that does not exist");
			return null;
		}
		Message res = new Message();
		res.what = AndroSoul.CONTACT_STATE_UPDATE;
		res.obj = c;
		return res;
	}

	/**
	 * Called when a contact sends a message
	 * 
	 * @param msg
	 *            Message containing the arguments
	 * @return New message to be displayed, containing referenced to the contact
	 *         in the dictionary, and message's content
	 */
	private Message handleReceiveMsg(Message msg) {
		String author = msg.getData().getString("login");
		String[] tmp = msg.getData().getString("command_ext").split(" ");
		String content = StringUtils.urlDecode(tmp[0]);

		MessageItem msg_item = new MessageItem(author, content);
		Message res = new Message();
		res.what = AndroSoul.RECEIVE_MESSAGE;
		res.obj = msg_item;

		// Add message in conversation
		NetsoulContact c = _dictionary.get(author);
		if (c == null) {
			c = _dictionary.createContact(author);
		}
		c.getConversation().receive(msg_item);

		// Notify the list of opened conversations
		_openedConversations.contactMessage(c);

		// arg1 contains the id of the contact. We must keep an integer id since
		// the notification manager only accepts integers.
		res.arg1 = c.getId();
		return res;
	}

	/**
	 * push a new query in the query queue
	 * 
	 * @param q
	 *            Query to be pushed
	 */
	private void PushQuery(Query q) {
		try {
			_queryQueue.push(q);
		} catch (AndroSoulException e) {
			Message _m = new Message();
			_m.what = AndroSoul.EXCEPTION;
			_m.obj = e;
			_userHandler.sendMessage(_m);
		} catch (java.lang.NullPointerException e) {
			Log.e(LOGCAT, "[AndroSoul][PushQuery] NullPointerException  ");
		}
	}

	/**
	 * Send the authentication query after receiving the authentication
	 * agreement. Used data given by the server to generate the query.
	 * 
	 * @param login
	 *            Login used in authentication
	 * @param pass
	 *            Password used in authentication
	 */
	private void sendAuthQuery(String login, String pass) {
		// Send login command of this form
		// ext_user_log <login user> <md5> <user data> <user location>

		String tmp = _connection.getMd5() + "-" + _connection.getHostClient()
				+ "/" + String.valueOf(_connection.getPortClient()) + pass;
		String md5 = StringUtils.md5(tmp);
		PushQuery(new Query(AndroSoul.EXT_USER_LOG, "ext_user_log " + login
				+ " " + md5 + " " + StringUtils.urlEncode(_location) + " "
				+ StringUtils.urlEncode(_comment), true));
	}

	/**
	 * Send a message to a contact
	 * 
	 * @param c
	 *            Contact for which to send the message
	 * @param content
	 *            Content of the message
	 */
	public void sendContactMsg(NetsoulContact c, String content) {
		if (_connection.getState() != Connection.State.AUTHENTIFIED)
			return;

		String tmp = "user_cmd msg_user " + c.getName() + " msg "
				+ StringUtils.urlEncode(content);
		PushQuery(new Query(AndroSoul.NONE, tmp, false));

		c.getConversation().addMessage(
				new MessageItem(_myself.getName(), content));
	}

	public void sendContactIsTyping(NetsoulContact c) {
		String tmp = "user_cmd msg_user " + c.getName()
				+ " dotnetSoul_UserTyping " + "null";
		PushQuery(new Query(AndroSoul.NONE, tmp, false));
	}

	public void sendContactStopTyping(NetsoulContact c) {
		String tmp = "user_cmd msg_user " + c.getName()
				+ " dotnetSoul_UserCancelledTyping " + "null";
		PushQuery(new Query(AndroSoul.NONE, tmp, false));
	}

	/**
	 * Send a wizz to a contact. The contact will receive the wizz only if it is
	 * using the AndroSoul client.
	 * 
	 * @param c
	 *            Contact for which to send a wizz
	 */
	public void sendContactWizz(NetsoulContact c) {
		String tmp = "user_cmd msg_user " + c.getName() + " wizz null";
		PushQuery(new Query(AndroSoul.NONE, tmp, false));
	}

	/**
	 * Send the query that asks the server for authentication
	 */
	private void askForAuth() {
		PushQuery(new Query(AndroSoul.AUTH_AG, "auth_ag ext_user none none",
				true));
	}

	/**
	 * Launch query asking to be notified when a user changes its status.
	 */
	public void updateWatchLog() {
		updateWatchLog(_dictionary.generateNetsoulList());
	}

	public void updateWatchLog(String items) {
		String str = "user_cmd watch_log_user {" + items + "}";
		Query q = new Query(AndroSoul.LIST_USERS, str, false);
		PushQuery(q);
	}

	/**
	 * Add a group in the contact list
	 * 
	 * @param name
	 *            Name of the group to be added
	 * @return reference to the newly added group
	 */
	public ContactGroup addGroup(String name) {
		return _contacts.addGroup(name);
	}

	/**
	 * Add a contact to the given group
	 * 
	 * @param name
	 *            Name of the contact
	 * @param group
	 *            Group in which to add the contact
	 */
	public NetsoulContact addContact(String name, ContactGroup group) {
		NetsoulContact c = _contacts.addContact(name, group);
		c.setConversation(new Conversation(c, this));
		// Request status for new contact
		sendListUsers(name);
		// Update the watch dog
		updateWatchLog(name);

		return c;
	}

	public void sendListUsers(String name) {
		Query q = new Query(AndroSoul.LIST_USERS, "list_users {" + name + "}",
				true);
		PushQuery(q);
	}

	public void addContacts(List<String> list, ContactGroup group) {

		String join = "";
		for (String e : list) {
			NetsoulContact c = _contacts.addContact(e, group);
			c.setConversation(new Conversation(c, this));
			join += e + ",";
		}
		sendListUsers(join);
		// Update the watch dog
		updateWatchLog(join);
	}

	/**
	 * Ask the server for the locations / state of all the contacts. Called
	 * after authentication. The later updates should be done thanks to the
	 * watch log command.
	 */
	public void AskForAllContactsLocations() {
		String tmp = _dictionary.generateNetsoulList();
		Query q = new Query(AndroSoul.LIST_USERS, "list_users {" + tmp + "}",
				true);
		PushQuery(q);
	}

	/**
	 * Find a group by name
	 * 
	 * @param name
	 *            Name of the group
	 * @return Group found, or null
	 */
	public ContactGroup findGroup(String name) {
		return _contacts.findGroup(name);
	}

	/**
	 * Delete the contact that belongs to the given group. If the group is not
	 * specified, delete the contact in each group.
	 * 
	 * @param c
	 *            Contact to be deleted
	 * @param g
	 *            Group in which to delete the contact
	 */
	public void deleteContact(NetsoulContact c, ContactGroup g) {
		// if no group is specified, delete everywhere
		if (g == null) {
			Iterator<ContactGroup> it = _contacts.iterator();
			while (it.hasNext()) {
				it.next().getList().remove(c);
			}
			return;
		}
		g.getList().remove(c);
	}

	/**
	 * Load contacts from the given stream
	 * 
	 * @param fis
	 *            input stream
	 */
	public void contactsLoad(FileInputStream fis) {
		_contacts.clear();
		if (fis != null) {
			ContactsLoader loader;
			try {
				loader = new ContactsLoader(this, fis);
				loader.load(_contacts);
			} catch (StreamCorruptedException e) {
				Log.e(LOGCAT, "Error while loading contacts");
			} catch (IOException e) {
				Log.e(LOGCAT, "Error while loading contacts: no such file");
			}
		}
		// Add the default group if not group exists
		if (_contacts.isEmpty())
			_contacts.addGroup(DEFAULT_GROUP_NAME);
		updateWatchLog();
		AskForAllContactsLocations();
	}

	/**
	 * Save contacts to the given stream
	 * 
	 * @param fos
	 *            output stream
	 */
	public void contactsSave(FileOutputStream fos) {
		ContactsSaver saver = new ContactsSaver(fos);
		saver.save(_contacts);
		Log.v(AndroSoul.LOGCAT, "Saved contacts");
	}

	/**
	 * Print contact list (for debugging)
	 */
	public void contactsPrint() {
		Printer printer = new Printer();
		printer.print(_contacts);
	}

	public void contactsClear() {
		_contacts.clear();
	}

	/**
	 * Change the state of the user
	 * 
	 * @param state
	 *            State to be set
	 */
	public void changeUserState(String state) {
		_myself.setState(state);
		PushQuery(new Query(AndroSoul.NONE, "user_cmd state " + state + ":"
				+ Calendar.getInstance().getTimeInMillis(), false));
	}

	public ContactDictionary getDictionary() {
		return _dictionary;
	}

	public Contacts getContacts() {
		return _contacts;
	}

	public OpenedConversations getOpenedConversations() {
		return _openedConversations;
	}

	/**
	 * @return The current connection's state
	 */
	public Connection.State getState() {
		if (_connection == null) {
			return Connection.State.OFF;
		}
		return _connection.getState();
	}

	/**
	 * Send the logout query to the server. Update the connection state. Clear
	 * the contacts locations.
	 */
	public void UserLogout() {
		_openedConversations.clear();
		_dictionary.clearLocations();
		PushQuery(new Query(EXIT, "exit", false));
		_connection.setState(Connection.State.LOGOUT);
		_connection.Close();
	}

	/*
	 * Get the list of groups in which a given contact belongs to TODO: optimize
	 * this
	 */
	public ArrayList<ContactGroup> getContactGroups(NetsoulContact contact) {
		ArrayList<ContactGroup> res = new ArrayList<ContactGroup>();

		for (ContactGroup e : _contacts.getGroups()) {
			if (e.getList().contains(contact)) {
				res.add(e);
			}
		}

		return res;
	}

	public Boolean renameGroup(ContactGroup group, String name) {
		/* Return false if a group with this name already exists */
		if (null != findGroup(name)) {
			return false;
		}

		group.setName(name);
		return true;
	}

	public void deleteGroup(ContactGroup group) {
		_contacts.getGroups().remove(group);
	}

	public void enableWizz(boolean value) {
		_enableWizzs = value;
	}

	public String get_comment() {
		return _comment;
	}

	public void set_comment(String comment) {
		this._comment = comment;
	}

	public String get_location() {
		return _location;
	}

	public void set_location(String _location) {
		this._location = _location;
	}

	public void ping() {
		PushQuery(new Query(NONE, "ping", false));
	}
}
